/**
 * 
 */
package com.ncr.travel.tas.testing;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.MethodDescriptor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.joda.time.DateTime;
import org.mockito.Mockito;
import org.mockito.exceptions.base.MockitoException;

/**
 * @author Rob Cooke
 */
public class TestUtilities {
	private static Map<Class<?>, Object> defaultValues = createDefaultValuesMap();
	
	/**
	 * @return
	 */
	private static Map<Class<?>, Object> createDefaultValuesMap() {
		Map<Class<?>, Object> values = new HashMap<Class<?>, Object>();
		values.put(String.class, "test string");
		values.put(byte.class, (byte)0xFF);
		values.put(short.class, 0);
		values.put(int.class, 0);
		values.put(long.class,  0L);
		values.put(float.class, 0F);
		values.put(double.class, 0D);
		values.put(char.class, 'q');
		values.put(java.lang.Character.class, 'q');
		values.put(boolean.class, false);
		values.put(java.lang.Boolean.class, false);
		values.put(org.joda.time.DateTime.class, new DateTime());
		return values;
	}
	
	/**
	 * @param target
	 * @param propertyName
	 */
	public static void testSimpleProperty(Object target, String propertyName) {
		testSimpleProperty(target, propertyName, null);
	}

	/**
	 * @param target
	 * @param propertyName
	 * @param propertyArgument
	 */
	public static void testSimpleProperty(Object target, String propertyName, Object propertyArgument) {
		try {
			PropertyDescriptor descriptor = new PropertyDescriptor(propertyName, target.getClass());

			// If there wasn't any argument supplied, create one.
			if (propertyArgument == null) {
				Class<?> type = descriptor.getPropertyType();
				propertyArgument = getTestArgument(type);
			}

			// Get the 'get' and 'set' methods.
			Method writeMethod = descriptor.getWriteMethod();
			Method readMethod = descriptor.getReadMethod();

			// Test them.
			writeMethod.invoke(target, propertyArgument);
			Object propertyValue = readMethod.invoke(target);
			assertEquals(propertyArgument, propertyValue);
		} catch (MockitoException e) {
			fail("Could not create a mock parameter for " + propertyName + ". Details: " + e.getLocalizedMessage());
		} catch (IntrospectionException e) {
			fail("Error creating MethodDescriptor(s) for property [" + propertyName + "].");
		} catch (IllegalAccessException e) {
			fail("Error accessing property methods. Are the methods on the class under test accessible?");
		} catch (InvocationTargetException e) {
			fail("Error invoking method on target.");
		}
	}

	/**
	 * @param target
	 * @param propertyItemName
	 * @param pluralizedItemName
	 */
	public static void testListProperty(Object target, String propertyItemName, String pluralizedItemName) {
		testListProperty(target, propertyItemName, pluralizedItemName, null);
	}
	
	/**
	 * @param target
	 * @param propertyItemName
	 * @param pluralizedItemName
	 * @param propertyArgument
	 */
	public static void testListProperty(Object target, String propertyItemName, String pluralizedItemName, Object propertyArgument) {
		try {
			// Capitalize the names in such a way that they can be prefixed with
			// a 'get', 'add', or 'remove'.
			propertyItemName = capitalizePropertyName(propertyItemName);
			pluralizedItemName = capitalizePropertyName(pluralizedItemName);

			// Construct the expected method names.
			String getMethodName = "get" + pluralizedItemName;
			String addMethodName = "add" + propertyItemName;
			String removeMethodName = "remove" + propertyItemName;

			// Grab all the methods from the class under test.
			BeanInfo beanInfo = Introspector.getBeanInfo(target.getClass());
			MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();

			// Search the methods for method names that match our expected
			// methods.
			Method getMethod = null;
			Method addMethod = null;
			Method removeMethod = null;
			for (MethodDescriptor methodDescriptor : methodDescriptors) {
				String name = methodDescriptor.getName();
				if (name.equals(getMethodName)) {
					getMethod = methodDescriptor.getMethod();
				} else if (name.equals(addMethodName)) {
					addMethod = methodDescriptor.getMethod();
				} else if (name.equals(removeMethodName)) {
					removeMethod = methodDescriptor.getMethod();
				}
			}

			// Test the 'get' method.
			if (getMethod != null) {
				testListGet(target, getMethod);

				// Test the 'add' method.
				if (addMethod != null) {
					testListAdd(target, getMethod, addMethod, propertyArgument);

					// Test the 'remove' method.
					if (removeMethod != null) {
						testListRemove(target, getMethod, addMethod, removeMethod, propertyArgument);
					}
				}
			} else {
				fail("The collection property does not have a 'get' method. This test requires one.");
			}
		} catch (IntrospectionException e) {
			fail("Error creating MethodDescriptor(s) for property [" + propertyItemName + "].");
		}
	}

	/**
	 * @param target
	 * @param getMethod
	 */
	private static void testListGet(Object target, Method getMethod) {
		try {
			List<?> list = (List<?>) getMethod.invoke(target);
			assertNotNull("The list accessor (" + getMethod.getName() + ") returned a null list. Are you missing an initialization in the constructor of the class under test?", list);
		} catch (InvocationTargetException e) {
			fail("Error invoking method on target.");
		} catch (IllegalAccessException e) {
			fail("Error accessing property methods. Are the methods on the class under test accessible?");
		}
	}

	/**
	 * @param target
	 * @param getMethod
	 * @param addMethod
	 * @param propertyArgument
	 */
	private static void testListAdd(Object target, Method getMethod, Method addMethod, Object propertyArgument) {
		try {
			// If we were not given an item to attempt to add, mock one.
			if (propertyArgument == null) {
				Class<?>[] parameterTypes = addMethod.getParameterTypes();
				if (parameterTypes.length != 1) {
					fail("Collection property 'add' method, " + addMethod.getName() + " does not accept exactly one parameter.");
				}
				
				propertyArgument = getTestArgument(parameterTypes[0]);
			}
			
			List<?> list = (List<?>) getMethod.invoke(target);
			assertNotNull(list);
			int initialSize = list.size();

			// Add the item.
			addMethod.invoke(target, propertyArgument);

			// Check the collection size.
			list = (List<?>) getMethod.invoke(target);
			assertNotNull(list);
			assertTrue(list.size() == initialSize + 1);

			// Check the last value.
			assertEquals(propertyArgument, list.get(list.size() - 1));
		} catch (InvocationTargetException e) {
			fail("Error invoking method on target.");
		} catch (IllegalAccessException e) {
			fail("Error accessing property methods. Are the methods on the class under test accessible?");
		}
	}

	/**
	 * @param target
	 * @param getMethod
	 * @param addMethod
	 * @param removeMethod
	 * @param propertyArgument
	 */
	private static void testListRemove(Object target, Method getMethod, Method addMethod, Method removeMethod, Object propertyArgument) {
		try {
			if (propertyArgument == null) {
				Class<?>[] parameterTypes = addMethod.getParameterTypes();
				if (parameterTypes.length != 1) {
					fail("Collection property 'add' method, " + addMethod.getName() + " does not accept exactly one parameter.");
				}
				
				propertyArgument = getTestArgument(parameterTypes[0]);
			}
			
			// Check the collection size.
			List<?> list = (List<?>) getMethod.invoke(target);
			int currentSize = list.size();

			// Add the item.
			addMethod.invoke(target, propertyArgument);

			// Check the collection size.
			list = (List<?>) getMethod.invoke(target);
			assertNotNull(list);
			assertTrue(list.size() == currentSize + 1);

			// Check the last value.
			assertEquals(propertyArgument, list.get(list.size() - 1));

			// Remove an item.
			removeMethod.invoke(target, list.size() - 1);
			list = (List<?>) getMethod.invoke(target);
			assertNotNull(list);
			assertTrue(list.size() == currentSize);
		} catch (InvocationTargetException e) {
			fail("Error invoking method on target.");
		} catch (IllegalAccessException e) {
			fail("Error accessing property methods. Are the methods on the class under test accessible?");
		}
	}

	/**
	 * @param name
	 * @return
	 */
	public static String capitalizePropertyName(String name) {
		name = Introspector.decapitalize(name);
		char first = Character.toUpperCase(name.charAt(0));
		return (name.length() > 1) ? (first + name.substring(1)) : Character.toString(first);
	}

	/**
	 * @param type
	 * @return
	 */
	public static Object getTestArgument(Class<?> type) {
		Object argumentValue = null;
		if (type.isAnonymousClass()) {
			fail("Anonymous types are not supported.");
		}
		
		argumentValue = defaultValues.get(type);
		if (argumentValue == null) {
			argumentValue = Mockito.mock(type);
		}

		return argumentValue;
	}
}
